Passed
Push — master ( 1769fa...d9ce30 )
by Rafael S.
03:30
created

WaveFile.getChunkId_   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from 'bitdepth';
33
import * as imaadpcm from 'imaadpcm';
34
import * as alawmulaw from 'alawmulaw';
35
import {encode, decode} from 'base64-arraybuffer-es6';
36
import RIFFFile from './lib/riff-file';
37
import writeString from './lib/write-string';
38
import dwChannelMask from './lib/dw-channel-mask';
39
import interleave from './lib/interleave';
40
import truncateSamples from './lib/truncate-samples';
41
import fixRIFFTag from './lib/fix-riff-tag';
42
import {unpackArray, packArrayTo, unpackArrayTo,
43
  unpack, packTo, packStringTo, packString, pack} from 'byte-data';
44
45
/**
46
 * A class to read, write and process wav files.
47
 */
48
export default class WaveFile extends RIFFFile {
49
50
  /**
51
   * @param {?Uint8Array=} wavBuffer A wave file buffer.
52
   * @throws {Error} If container is not RIFF, RIFX or RF64.
53
   * @throws {Error} If format is not WAVE.
54
   * @throws {Error} If no 'fmt ' chunk is found.
55
   * @throws {Error} If no 'data' chunk is found.
56
   */
57
  constructor(wavBuffer=null) {
58
    super();
59
    /**
60
     * Audio formats.
61
     * Formats not listed here should be set to 65534,
62
     * the code for WAVE_FORMAT_EXTENSIBLE
63
     * @enum {number}
64
     * @private
65
     */
66
    this.WAV_AUDIO_FORMATS = {
67
      '4': 17,
68
      '8': 1,
69
      '8a': 6,
70
      '8m': 7,
71
      '16': 1,
72
      '24': 1,
73
      '32': 1,
74
      '32f': 3,
75
      '64': 3
76
    };
77
    /**
78
     * The data of the 'fmt' chunk.
79
     * @type {!Object<string, *>}
80
     */
81
    this.fmt = {
82
      /** @type {string} */
83
      chunkId: '',
84
      /** @type {number} */
85
      chunkSize: 0,
86
      /** @type {number} */
87
      audioFormat: 0,
88
      /** @type {number} */
89
      numChannels: 0,
90
      /** @type {number} */
91
      sampleRate: 0,
92
      /** @type {number} */
93
      byteRate: 0,
94
      /** @type {number} */
95
      blockAlign: 0,
96
      /** @type {number} */
97
      bitsPerSample: 0,
98
      /** @type {number} */
99
      cbSize: 0,
100
      /** @type {number} */
101
      validBitsPerSample: 0,
102
      /** @type {number} */
103
      dwChannelMask: 0,
104
      /**
105
       * 4 32-bit values representing a 128-bit ID
106
       * @type {!Array<number>}
107
       */
108
      subformat: []
109
    };
110
    /**
111
     * The data of the 'fact' chunk.
112
     * @type {!Object<string, *>}
113
     */
114
    this.fact = {
115
      /** @type {string} */
116
      chunkId: '',
117
      /** @type {number} */
118
      chunkSize: 0,
119
      /** @type {number} */
120
      dwSampleLength: 0
121
    };
122
    /**
123
     * The data of the 'cue ' chunk.
124
     * @type {!Object<string, *>}
125
     */
126
    this.cue = {
127
      /** @type {string} */
128
      chunkId: '',
129
      /** @type {number} */
130
      chunkSize: 0,
131
      /** @type {number} */
132
      dwCuePoints: 0,
133
      /** @type {!Array<!Object>} */
134
      points: [],
135
    };
136
    /**
137
     * The data of the 'smpl' chunk.
138
     * @type {!Object<string, *>}
139
     */
140
    this.smpl = {
141
      /** @type {string} */
142
      chunkId: '',
143
      /** @type {number} */
144
      chunkSize: 0,
145
      /** @type {number} */
146
      dwManufacturer: 0,
147
      /** @type {number} */
148
      dwProduct: 0,
149
      /** @type {number} */
150
      dwSamplePeriod: 0,
151
      /** @type {number} */
152
      dwMIDIUnityNote: 0,
153
      /** @type {number} */
154
      dwMIDIPitchFraction: 0,
155
      /** @type {number} */
156
      dwSMPTEFormat: 0,
157
      /** @type {number} */
158
      dwSMPTEOffset: 0,
159
      /** @type {number} */
160
      dwNumSampleLoops: 0,
161
      /** @type {number} */
162
      dwSamplerData: 0,
163
      /** @type {!Array<!Object>} */
164
      loops: []
165
    };
166
    /**
167
     * The data of the 'bext' chunk.
168
     * @type {!Object<string, *>}
169
     */
170
    this.bext = {
171
      /** @type {string} */
172
      chunkId: '',
173
      /** @type {number} */
174
      chunkSize: 0,
175
      /** @type {string} */
176
      description: '', //256
177
      /** @type {string} */
178
      originator: '', //32
179
      /** @type {string} */
180
      originatorReference: '', //32
181
      /** @type {string} */
182
      originationDate: '', //10
183
      /** @type {string} */
184
      originationTime: '', //8
185
      /**
186
       * 2 32-bit values, timeReference high and low
187
       * @type {!Array<number>}
188
       */
189
      timeReference: [0, 0],
190
      /** @type {number} */
191
      version: 0, //WORD
192
      /** @type {string} */
193
      UMID: '', // 64 chars
194
      /** @type {number} */
195
      loudnessValue: 0, //WORD
196
      /** @type {number} */
197
      loudnessRange: 0, //WORD
198
      /** @type {number} */
199
      maxTruePeakLevel: 0, //WORD
200
      /** @type {number} */
201
      maxMomentaryLoudness: 0, //WORD
202
      /** @type {number} */
203
      maxShortTermLoudness: 0, //WORD
204
      /** @type {string} */
205
      reserved: '', //180
206
      /** @type {string} */
207
      codingHistory: '' // string, unlimited
208
    };
209
    /**
210
     * The data of the 'ds64' chunk.
211
     * Used only with RF64 files.
212
     * @type {!Object<string, *>}
213
     */
214
    this.ds64 = {
215
      /** @type {string} */
216
      chunkId: '',
217
      /** @type {number} */
218
      chunkSize: 0,
219
      /** @type {number} */
220
      riffSizeHigh: 0, // DWORD
221
      /** @type {number} */
222
      riffSizeLow: 0, // DWORD
223
      /** @type {number} */
224
      dataSizeHigh: 0, // DWORD
225
      /** @type {number} */
226
      dataSizeLow: 0, // DWORD
227
      /** @type {number} */
228
      originationTime: 0, // DWORD
229
      /** @type {number} */
230
      sampleCountHigh: 0, // DWORD
231
      /** @type {number} */
232
      sampleCountLow: 0 // DWORD
233
      /** @type {number} */
234
      //'tableLength': 0, // DWORD
235
      /** @type {!Array<number>} */
236
      //'table': []
237
    };
238
    /**
239
     * The data of the 'data' chunk.
240
     * @type {!Object<string, *>}
241
     */
242
    this.data = {
243
      /** @type {string} */
244
      chunkId: '',
245
      /** @type {number} */
246
      chunkSize: 0,
247
      /** @type {!Uint8Array} */
248
      samples: new Uint8Array(0)
249
    };
250
    /**
251
     * The data of the 'LIST' chunks.
252
     * Each item in this list look like this:
253
     *  {
254
     *      chunkId: '',
255
     *      chunkSize: 0,
256
     *      format: '',
257
     *      subChunks: []
258
     *   }
259
     * @type {!Array<!Object>}
260
     */
261
    this.LIST = [];
262
    /**
263
     * The data of the 'junk' chunk.
264
     * @type {!Object<string, *>}
265
     */
266
    this.junk = {
267
      /** @type {string} */
268
      chunkId: '',
269
      /** @type {number} */
270
      chunkSize: 0,
271
      /** @type {!Array<number>} */
272
      chunkData: []
273
    };
274
    /**
275
     * The bit depth code according to the samples.
276
     * @type {string}
277
     */
278
    this.bitDepth = '0';
279
    /**
280
     * @type {!Object}
281
     * @private
282
     */
283
    this.dataType = {};
284
    // Load a file from the buffer if one was passed
285
    // when creating the object
286
    if (wavBuffer) {
287
      this.fromBuffer(wavBuffer);
288
    }
289
  }
290
291
  /**
292
   * Return the sample at a given index.
293
   * @param {number} index The sample index.
294
   * @return {number} The sample.
295
   * @throws {Error} If the sample index is off range.
296
   */
297
  getSample(index) {
298
    index = index * (this.dataType.bits / 8);
299
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
300
      throw new Error('Range error');
301
    }
302
    return unpack(
303
      this.data.samples.slice(index, index + this.dataType.bits / 8),
304
      this.dataType);
305
  }
306
307
  /**
308
   * Set the sample at a given index.
309
   * @param {number} index The sample index.
310
   * @param {number} sample The sample.
311
   * @throws {Error} If the sample index is off range.
312
   */
313
  setSample(index, sample) {
314
    index = index * (this.dataType.bits / 8);
315
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
316
      throw new Error('Range error');
317
    }
318
    packTo(sample, this.dataType, this.data.samples, index);
319
  }
320
321
  /**
322
   * Set up the WaveFile object based on the arguments passed.
323
   * @param {number} numChannels The number of channels
324
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
325
   * @param {number} sampleRate The sample rate.
326
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
327
   * @param {string} bitDepthCode The audio bit depth code.
328
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
329
   *    or any value between '8' and '32' (like '12').
330
   * @param {!Array<number>|!Array<!Array<number>>|!TypedArray} samples
331
   *    The samples. Must be in the correct range according to the bit depth.
332
   * @param {?Object} options Optional. Used to force the container
333
   *    as RIFX with {'container': 'RIFX'}
334
   * @throws {Error} If any argument does not meet the criteria.
335
   */
336
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
337
    if (!options.container) {
338
      options.container = 'RIFF';
339
    }
340
    this.container = options.container;
341
    this.bitDepth = bitDepthCode;
342
    samples = interleave(samples);
343
    this.updateDataType_();
344
    /** @type {number} */
345
    let numBytes = this.dataType.bits / 8;
346
    this.data.samples = new Uint8Array(samples.length * numBytes);
347
    packArrayTo(samples, this.dataType, this.data.samples);
348
    this.clearHeader_();
349
    this.makeWavHeader(
350
      bitDepthCode, numChannels, sampleRate,
351
      numBytes, this.data.samples.length, options);
352
    this.data.chunkId = 'data';
353
    this.data.chunkSize = this.data.samples.length;
354
    this.validateWavHeader_();
355
  }
356
357
  /**
358
   * Set up the WaveFile object from a byte buffer.
359
   * @param {!Uint8Array} bytes The buffer.
360
   * @param {boolean=} samples True if the samples should be loaded.
361
   * @throws {Error} If container is not RIFF, RIFX or RF64.
362
   * @throws {Error} If format is not WAVE.
363
   * @throws {Error} If no 'fmt ' chunk is found.
364
   * @throws {Error} If no 'data' chunk is found.
365
   */
366
  fromBuffer(bytes, samples=true) {
367
    this.clearHeader_();
368
    this.readWavBuffer(bytes, samples);
369
    this.bitDepthFromFmt_();
370
    this.updateDataType_();
371
  }
372
373
  /**
374
   * Return a byte buffer representig the WaveFile object as a .wav file.
375
   * The return value of this method can be written straight to disk.
376
   * @return {!Uint8Array} A .wav file.
377
   * @throws {Error} If any property of the object appears invalid.
378
   */
379
  toBuffer() {
380
    this.validateWavHeader_();
381
    return this.writeWavBuffer();
382
  }
383
384
  /**
385
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
386
   * @param {string} base64String A .wav file as a base64 string.
387
   * @throws {Error} If any property of the object appears invalid.
388
   */
389
  fromBase64(base64String) {
390
    this.fromBuffer(new Uint8Array(decode(base64String)));
391
  }
392
393
  /**
394
   * Return a base64 string representig the WaveFile object as a .wav file.
395
   * @return {string} A .wav file as a base64 string.
396
   * @throws {Error} If any property of the object appears invalid.
397
   */
398
  toBase64() {
399
    /** @type {!Uint8Array} */
400
    let buffer = this.toBuffer();
401
    return encode(buffer, 0, buffer.length);
402
  }
403
404
  /**
405
   * Return a DataURI string representig the WaveFile object as a .wav file.
406
   * The return of this method can be used to load the audio in browsers.
407
   * @return {string} A .wav file as a DataURI.
408
   * @throws {Error} If any property of the object appears invalid.
409
   */
410
  toDataURI() {
411
    return 'data:audio/wav;base64,' + this.toBase64();
412
  }
413
414
  /**
415
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
416
   * @param {string} dataURI A .wav file as DataURI.
417
   * @throws {Error} If any property of the object appears invalid.
418
   */
419
  fromDataURI(dataURI) {
420
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
421
  }
422
423
  /**
424
   * Force a file as RIFF.
425
   */
426
  toRIFF() {
427
    this.fromScratch(
428
      this.fmt.numChannels,
429
      this.fmt.sampleRate,
430
      this.bitDepth,
431
      unpackArray(this.data.samples, this.dataType));
432
  }
433
434
  /**
435
   * Force a file as RIFX.
436
   */
437
  toRIFX() {
438
    this.fromScratch(
439
      this.fmt.numChannels,
440
      this.fmt.sampleRate,
441
      this.bitDepth,
442
      unpackArray(this.data.samples, this.dataType),
443
      {container: 'RIFX'});
444
  }
445
446
  /**
447
   * Change the bit depth of the samples.
448
   * @param {string} newBitDepth The new bit depth of the samples.
449
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
450
   * @param {boolean} changeResolution A boolean indicating if the
451
   *    resolution of samples should be actually changed or not.
452
   * @throws {Error} If the bit depth is not valid.
453
   */
454
  toBitDepth(newBitDepth, changeResolution=true) {
455
    /** @type {string} */
456
    let toBitDepth = newBitDepth;
457
    /** @type {string} */
458
    let thisBitDepth = this.bitDepth;
459
    if (!changeResolution) {
460
      if (newBitDepth != '32f') {
461
        toBitDepth = this.dataType.bits.toString();
462
      }
463
      thisBitDepth = this.dataType.bits;
464
    }
465
    this.assureUncompressed_();
466
    /** @type {number} */
467
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
468
    /** @type {!Float64Array} */
469
    let typedSamplesInput = new Float64Array(sampleCount);
470
    /** @type {!Float64Array} */
471
    let typedSamplesOutput = new Float64Array(sampleCount);
472
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
473
    if (thisBitDepth == "32f" || thisBitDepth == "64") {
474
      truncateSamples(typedSamplesInput);
475
    }
476
    bitDepthLib(
477
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
478
    this.fromScratch(
479
      this.fmt.numChannels,
480
      this.fmt.sampleRate,
481
      newBitDepth,
482
      typedSamplesOutput,
483
      {container: this.correctContainer_()});
484
  }
485
486
  /**
487
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
488
   * @throws {Error} If sample rate is not 8000.
489
   * @throws {Error} If number of channels is not 1.
490
   */
491
  toIMAADPCM() {
492
    if (this.fmt.sampleRate !== 8000) {
493
      throw new Error(
494
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
495
    } else if (this.fmt.numChannels !== 1) {
496
      throw new Error(
497
        'Only mono files can be compressed as IMA-ADPCM.');
498
    } else {
499
      this.assure16Bit_();
500
      /** @type {!Int16Array} */
501
      let output = new Int16Array(this.data.samples.length / 2);
502
      unpackArrayTo(this.data.samples, this.dataType, output);
503
      this.fromScratch(
504
        this.fmt.numChannels,
505
        this.fmt.sampleRate,
506
        '4',
507
        imaadpcm.encode(output),
508
        {container: this.correctContainer_()});
509
    }
510
  }
511
512
  /**
513
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
514
   * @param {string} bitDepthCode The new bit depth of the samples.
515
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
516
   *    Optional. Default is 16.
517
   */
518
  fromIMAADPCM(bitDepthCode='16') {
519
    this.fromScratch(
520
      this.fmt.numChannels,
521
      this.fmt.sampleRate,
522
      '16',
523
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
524
      {container: this.correctContainer_()});
525
    if (bitDepthCode != '16') {
526
      this.toBitDepth(bitDepthCode);
527
    }
528
  }
529
530
  /**
531
   * Encode a 16-bit wave file as 8-bit A-Law.
532
   */
533
  toALaw() {
534
    this.assure16Bit_();
535
    /** @type {!Int16Array} */
536
    let output = new Int16Array(this.data.samples.length / 2);
537
    unpackArrayTo(this.data.samples, this.dataType, output);
538
    this.fromScratch(
539
      this.fmt.numChannels,
540
      this.fmt.sampleRate,
541
      '8a',
542
      alawmulaw.alaw.encode(output),
543
      {container: this.correctContainer_()});
544
  }
545
546
  /**
547
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
548
   * @param {string} bitDepthCode The new bit depth of the samples.
549
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
550
   *    Optional. Default is 16.
551
   */
552
  fromALaw(bitDepthCode='16') {
553
    this.fromScratch(
554
      this.fmt.numChannels,
555
      this.fmt.sampleRate,
556
      '16',
557
      alawmulaw.alaw.decode(this.data.samples),
558
      {container: this.correctContainer_()});
559
    if (bitDepthCode != '16') {
560
      this.toBitDepth(bitDepthCode);
561
    }
562
  }
563
564
  /**
565
   * Encode 16-bit wave file as 8-bit mu-Law.
566
   */
567
  toMuLaw() {
568
    this.assure16Bit_();
569
    /** @type {!Int16Array} */
570
    let output = new Int16Array(this.data.samples.length / 2);
571
    unpackArrayTo(this.data.samples, this.dataType, output);
572
    this.fromScratch(
573
      this.fmt.numChannels,
574
      this.fmt.sampleRate,
575
      '8m',
576
      alawmulaw.mulaw.encode(output),
577
      {container: this.correctContainer_()});
578
  }
579
580
  /**
581
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
582
   * @param {string} bitDepthCode The new bit depth of the samples.
583
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
584
   *    Optional. Default is 16.
585
   */
586
  fromMuLaw(bitDepthCode='16') {
587
    this.fromScratch(
588
      this.fmt.numChannels,
589
      this.fmt.sampleRate,
590
      '16',
591
      alawmulaw.mulaw.decode(this.data.samples),
592
      {container: this.correctContainer_()});
593
    if (bitDepthCode != '16') {
594
      this.toBitDepth(bitDepthCode);
595
    }
596
  }
597
598
  /**
599
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
600
   * then it is created. It if exists, it is overwritten.
601
   * @param {string} tag The tag name.
602
   * @param {string} value The tag value.
603
   * @throws {Error} If the tag name is not valid.
604
   */
605
  setTag(tag, value) {
606
    tag = fixRIFFTag(tag);
607
    /** @type {!Object} */
608
    let index = this.getTagIndex_(tag);
609
    if (index.TAG !== null) {
610
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
611
        value.length + 1;
612
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
613
    } else if (index.LIST !== null) {
614
      this.LIST[index.LIST].subChunks.push({
615
        chunkId: tag,
616
        chunkSize: value.length + 1,
617
        value: value});
618
    } else {
619
      this.LIST.push({
620
        chunkId: 'LIST',
621
        chunkSize: 8 + value.length + 1,
622
        format: 'INFO',
623
        subChunks: []});
624
      this.LIST[this.LIST.length - 1].subChunks.push({
625
        chunkId: tag,
626
        chunkSize: value.length + 1,
627
        value: value});
628
    }
629
  }
630
631
  /**
632
   * Return the value of a RIFF tag in the INFO chunk.
633
   * @param {string} tag The tag name.
634
   * @return {?string} The value if the tag is found, null otherwise.
635
   */
636
  getTag(tag) {
637
    /** @type {!Object} */
638
    let index = this.getTagIndex_(tag);
639
    if (index.TAG !== null) {
640
      return this.LIST[index.LIST].subChunks[index.TAG].value;
641
    }
642
    return null;
643
  }
644
645
  /**
646
   * Return a Object<tag, value> with the RIFF tags in the file.
647
   * @return {!Object<string, string>} The file tags.
648
   */
649
  listTags() {
650
    /** @type {?number} */
651
    let index = this.getLISTINFOIndex_();
652
    /** @type {!Object} */
653
    let tags = {};
654
    if (index !== null) {
655
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
656
        tags[this.LIST[index].subChunks[i].chunkId] =
657
          this.LIST[index].subChunks[i].value;
658
      }
659
    }
660
    return tags;
661
  }
662
663
  /**
664
   * Remove a RIFF tag in the INFO chunk.
665
   * @param {string} tag The tag name.
666
   * @return {boolean} True if a tag was deleted.
667
   */
668
  deleteTag(tag) {
669
    /** @type {!Object} */
670
    let index = this.getTagIndex_(tag);
671
    if (index.TAG !== null) {
672
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
673
      return true;
674
    }
675
    return false;
676
  }
677
678
  /**
679
   * Create a cue point in the wave file.
680
   * @param {number} position The cue point position in milliseconds.
681
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
682
   */
683
  setCuePoint(position, labl='') {
684
    this.cue.chunkId = 'cue ';
685
    position = (position * this.fmt.sampleRate) / 1000;
686
    /** @type {!Array<!Object>} */
687
    let existingPoints = this.getCuePoints_();
688
    this.clearLISTadtl_();
689
    /** @type {number} */
690
    let len = this.cue.points.length;
691
    this.cue.points = [];
692
    /** @type {boolean} */
693
    let hasSet = false;
694
    if (len === 0) {
695
      this.setCuePoint_(position, 1, labl);
696
    } else {
697
      for (let i = 0; i < len; i++) {
698
        if (existingPoints[i].dwPosition > position && !hasSet) {
699
          this.setCuePoint_(position, i + 1, labl);
700
          this.setCuePoint_(
701
            existingPoints[i].dwPosition,
702
            i + 2,
703
            existingPoints[i].label);
704
          hasSet = true;
705
        } else {
706
          this.setCuePoint_(
707
            existingPoints[i].dwPosition,
708
            i + 1,
709
            existingPoints[i].label);
710
        }
711
      }
712
      if (!hasSet) {
713
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
714
      }
715
    }
716
    this.cue.dwCuePoints = this.cue.points.length;
717
  }
718
719
  /**
720
   * Remove a cue point from a wave file.
721
   * @param {number} index the index of the point. First is 1,
722
   *    second is 2, and so on.
723
   */
724
  deleteCuePoint(index) {
725
    this.cue.chunkId = 'cue ';
726
    /** @type {!Array<!Object>} */
727
    let existingPoints = this.getCuePoints_();
728
    this.clearLISTadtl_();
729
    /** @type {number} */
730
    let len = this.cue.points.length;
731
    this.cue.points = [];
732
    for (let i = 0; i < len; i++) {
733
      if (i + 1 !== index) {
734
        this.setCuePoint_(
735
          existingPoints[i].dwPosition,
736
          i + 1,
737
          existingPoints[i].label);
738
      }
739
    }
740
    this.cue.dwCuePoints = this.cue.points.length;
741
    if (this.cue.dwCuePoints) {
742
      this.cue.chunkId = 'cue ';
743
    } else {
744
      this.cue.chunkId = '';
745
      this.clearLISTadtl_();
746
    }
747
  }
748
749
  /**
750
   * Return an array with all cue points in the file, in the order they appear
751
   * in the file.
752
   * The difference between this method and using the list in WaveFile.cue
753
   * is that the return value of this method includes the position in
754
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
755
   * @return {!Array<!Object>}
756
   */
757
  listCuePoints() {
758
    /** @type {!Array<!Object>} */
759
    let points = this.getCuePoints_();
760
    for (let i = 0, len = points.length; i < len; i++) {
761
      points[i].milliseconds =
762
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
763
    }
764
    return points;
765
  }
766
767
  /**
768
   * Update the label of a cue point.
769
   * @param {number} pointIndex The ID of the cue point.
770
   * @param {string} label The new text for the label.
771
   */
772
  updateLabel(pointIndex, label) {
773
    /** @type {?number} */
774
    let cIndex = this.getAdtlChunk_();
775
    if (cIndex !== null) {
776
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
777
        if (this.LIST[cIndex].subChunks[i].dwName ==
778
            pointIndex) {
779
          this.LIST[cIndex].subChunks[i].value = label;
780
        }
781
      }
782
    }
783
  }
784
785
  /**
786
   * Set the string code of the bit depth based on the 'fmt ' chunk.
787
   * @private
788
   */
789
  bitDepthFromFmt_() {
790
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
791
      this.bitDepth = '32f';
792
    } else if (this.fmt.audioFormat === 6) {
793
      this.bitDepth = '8a';
794
    } else if (this.fmt.audioFormat === 7) {
795
      this.bitDepth = '8m';
796
    } else {
797
      this.bitDepth = this.fmt.bitsPerSample.toString();
798
    }
799
  }
800
  
801
  /**
802
   * Push a new cue point in this.cue.points.
803
   * @param {number} position The position in milliseconds.
804
   * @param {number} dwName the dwName of the cue point
805
   * @private
806
   */
807
  setCuePoint_(position, dwName, label) {
808
    this.cue.points.push({
809
      dwName: dwName,
810
      dwPosition: position,
811
      fccChunk: 'data',
812
      dwChunkStart: 0,
813
      dwBlockStart: 0,
814
      dwSampleOffset: position,
815
    });
816
    this.setLabl_(dwName, label);
817
  }
818
819
  /**
820
   * Return an array with all cue points in the file, in the order they appear
821
   * in the file.
822
   * @return {!Array<!Object>}
823
   * @private
824
   */
825
  getCuePoints_() {
826
    /** @type {!Array<!Object>} */
827
    let points = [];
828
    for (let i = 0, len = this.cue.points.length; i < len; i++) {
829
      points.push({
830
        dwPosition: this.cue.points[i].dwPosition,
831
        label: this.getLabelForCuePoint_(
832
          this.cue.points[i].dwName)});
833
    }
834
    return points;
835
  }
836
837
  /**
838
   * Return the label of a cue point.
839
   * @param {number} pointDwName The ID of the cue point.
840
   * @return {string}
841
   * @private
842
   */
843
  getLabelForCuePoint_(pointDwName) {
844
    /** @type {?number} */
845
    let cIndex = this.getAdtlChunk_();
846
    if (cIndex !== null) {
847
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
848
        if (this.LIST[cIndex].subChunks[i].dwName ==
849
            pointDwName) {
850
          return this.LIST[cIndex].subChunks[i].value;
851
        }
852
      }
853
    }
854
    return '';
855
  }
856
857
  /**
858
   * Clear any LIST chunk labeled as 'adtl'.
859
   * @private
860
   */
861
  clearLISTadtl_() {
862
    for (let i = 0, len = this.LIST.length; i < len; i++) {
863
      if (this.LIST[i].format == 'adtl') {
864
        this.LIST.splice(i);
865
      }
866
    }
867
  }
868
869
  /**
870
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
871
   * @param {number} dwName The ID of the cue point.
872
   * @param {string} label The label for the cue point.
873
   * @private
874
   */
875
  setLabl_(dwName, label) {
876
    /** @type {?number} */
877
    let adtlIndex = this.getAdtlChunk_();
878
    if (adtlIndex === null) {
879
      this.LIST.push({
880
        chunkId: 'LIST',
881
        chunkSize: 4,
882
        format: 'adtl',
883
        subChunks: []});
884
      adtlIndex = this.LIST.length - 1;
885
    }
886
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
887
  }
888
889
  /**
890
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
891
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
892
   * @param {number} dwName The ID of the cue point.
893
   * @param {string} label The label for the cue point.
894
   * @private
895
   */
896
  setLabelText_(adtlIndex, dwName, label) {
897
    this.LIST[adtlIndex].subChunks.push({
898
      chunkId: 'labl',
899
      chunkSize: label.length,
900
      dwName: dwName,
901
      value: label
902
    });
903
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
904
  }
905
906
  /**
907
   * Return the index of the 'adtl' LIST in this.LIST.
908
   * @return {?number}
909
   * @private
910
   */
911
  getAdtlChunk_() {
912
    for (let i = 0, len = this.LIST.length; i < len; i++) {
913
      if (this.LIST[i].format == 'adtl') {
914
        return i;
915
      }
916
    }
917
    return null;
918
  }
919
920
  /**
921
   * Return the index of the INFO chunk in the LIST chunk.
922
   * @return {?number} the index of the INFO chunk.
923
   * @private
924
   */
925
  getLISTINFOIndex_() {
926
    /** @type {?number} */
927
    let index = null;
928
    for (let i = 0, len = this.LIST.length; i < len; i++) {
929
      if (this.LIST[i].format === 'INFO') {
930
        index = i;
931
        break;
932
      }
933
    }
934
    return index;
935
  }
936
937
  /**
938
   * Return the index of a tag in a FILE chunk.
939
   * @param {string} tag The tag name.
940
   * @return {!Object<string, ?number>}
941
   *    Object.LIST is the INFO index in LIST
942
   *    Object.TAG is the tag index in the INFO
943
   * @private
944
   */
945
  getTagIndex_(tag) {
946
    /** @type {!Object<string, ?number>} */
947
    let index = {LIST: null, TAG: null};
948
    for (let i = 0, len = this.LIST.length; i < len; i++) {
949
      if (this.LIST[i].format == 'INFO') {
950
        index.LIST = i;
951
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
952
          if (this.LIST[i].subChunks[j].chunkId == tag) {
953
            index.TAG = j;
954
            break;
955
          }
956
        }
957
        break;
958
      }
959
    }
960
    return index;
961
  }
962
963
  /**
964
   * Reset attributes that should emptied when a file is
965
   * created with the fromScratch() or fromBuffer() methods.
966
   * @private
967
   */
968
  clearHeader_() {
969
    this.fmt.cbSize = 0;
970
    this.fmt.validBitsPerSample = 0;
971
    this.fact.chunkId = '';
972
    this.ds64.chunkId = '';
973
  }
974
975
  /**
976
   * Make the file 16-bit if it is not.
977
   * @private
978
   */
979
  assure16Bit_() {
980
    this.assureUncompressed_();
981
    if (this.bitDepth != '16') {
982
      this.toBitDepth('16');
983
    }
984
  }
985
986
  /**
987
   * Uncompress the samples in case of a compressed file.
988
   * @private
989
   */
990
  assureUncompressed_() {
991
    if (this.bitDepth == '8a') {
992
      this.fromALaw();
993
    } else if (this.bitDepth == '8m') {
994
      this.fromMuLaw();
995
    } else if (this.bitDepth == '4') {
996
      this.fromIMAADPCM();
997
    }
998
  }
999
1000
  /**
1001
   * Update the type definition used to read and write the samples.
1002
   * @private
1003
   */
1004
  updateDataType_() {
1005
    this.dataType = {
1006
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
1007
      fp: this.bitDepth == '32f' || this.bitDepth == '64',
1008
      signed: this.bitDepth != '8',
1009
      be: this.container == 'RIFX'
1010
    };
1011
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
1012
      this.dataType.bits = 8;
1013
      this.dataType.signed = false;
1014
    }
1015
  }
1016
1017
  /**
1018
   * Return 'RIFF' if the container is 'RF64', the current container name
1019
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
1020
   * @return {string}
1021
   * @private
1022
   */
1023
  correctContainer_() {
1024
    return this.container == 'RF64' ? 'RIFF' : this.container;
1025
  }
1026
1027
  /**
1028
   * Return a .wav file byte buffer with the data from the WaveFile object.
1029
   * The return value of this method can be written straight to disk.
1030
   * @return {!Uint8Array} The wav file bytes.
1031
   * @private
1032
   */
1033
  writeWavBuffer() {
1034
    this.uInt16_.be = this.container === 'RIFX';
1035
    this.uInt32_.be = this.uInt16_.be;
1036
    /** @type {!Array<!Array<number>>} */
1037
    let fileBody = [
1038
      this.getJunkBytes_(),
1039
      this.getDs64Bytes_(),
1040
      this.getBextBytes_(),
1041
      this.getFmtBytes_(),
1042
      this.getFactBytes_(),
1043
      packString(this.data.chunkId),
1044
      pack(this.data.samples.length, this.uInt32_),
1045
      this.data.samples,
1046
      this.getCueBytes_(),
1047
      this.getSmplBytes_(),
1048
      this.getLISTBytes_()
1049
    ];
1050
    /** @type {number} */
1051
    let fileBodyLength = 0;
1052
    for (let i=0; i<fileBody.length; i++) {
1053
      fileBodyLength += fileBody[i].length;
1054
    }
1055
    /** @type {!Uint8Array} */
1056
    let file = new Uint8Array(fileBodyLength + 12);
1057
    /** @type {number} */
1058
    let index = 0;
1059
    index = packStringTo(this.container, file, index);
1060
    index = packTo(fileBodyLength + 4, this.uInt32_, file, index);
1061
    index = packStringTo(this.format, file, index);
1062
    for (let i=0; i<fileBody.length; i++) {
1063
      file.set(fileBody[i], index);
1064
      index += fileBody[i].length;
1065
    }
1066
    return file;
1067
  }
1068
1069
  /**
1070
   * Return the bytes of the 'bext' chunk.
1071
   * @private
1072
   */
1073
  getBextBytes_() {
1074
    /** @type {!Array<number>} */
1075
    let bytes = [];
1076
    this.enforceBext_();
1077
    if (this.bext.chunkId) {
1078
      this.bext.chunkSize = 602 + this.bext.codingHistory.length;
1079
      bytes = bytes.concat(
1080
        packString(this.bext.chunkId),
1081
        pack(602 + this.bext.codingHistory.length, this.uInt32_),
1082
        writeString(this.bext.description, 256),
1083
        writeString(this.bext.originator, 32),
1084
        writeString(this.bext.originatorReference, 32),
1085
        writeString(this.bext.originationDate, 10),
1086
        writeString(this.bext.originationTime, 8),
1087
        pack(this.bext.timeReference[0], this.uInt32_),
1088
        pack(this.bext.timeReference[1], this.uInt32_),
1089
        pack(this.bext.version, this.uInt16_),
1090
        writeString(this.bext.UMID, 64),
1091
        pack(this.bext.loudnessValue, this.uInt16_),
1092
        pack(this.bext.loudnessRange, this.uInt16_),
1093
        pack(this.bext.maxTruePeakLevel, this.uInt16_),
1094
        pack(this.bext.maxMomentaryLoudness, this.uInt16_),
1095
        pack(this.bext.maxShortTermLoudness, this.uInt16_),
1096
        writeString(this.bext.reserved, 180),
1097
        writeString(
1098
          this.bext.codingHistory, this.bext.codingHistory.length));
1099
    }
1100
    return bytes;
1101
  }
1102
1103
  /**
1104
   * Make sure a 'bext' chunk is created if BWF data was created in a file.
1105
   * @private
1106
   */
1107
  enforceBext_() {
1108
    for (let prop in this.bext) {
1109
      if (this.bext.hasOwnProperty(prop)) {
1110
        if (this.bext[prop] && prop != 'timeReference') {
1111
          this.bext.chunkId = 'bext';
1112
          break;
1113
        }
1114
      }
1115
    }
1116
    if (this.bext.timeReference[0] || this.bext.timeReference[1]) {
1117
      this.bext.chunkId = 'bext';
1118
    }
1119
  }
1120
1121
  /**
1122
   * Return the bytes of the 'ds64' chunk.
1123
   * @return {!Array<number>} The 'ds64' chunk bytes.
1124
   * @private
1125
   */
1126
  getDs64Bytes_() {
1127
    /** @type {!Array<number>} */
1128
    let bytes = [];
1129
    if (this.ds64.chunkId) {
1130
      bytes = bytes.concat(
1131
        packString(this.ds64.chunkId),
1132
        pack(this.ds64.chunkSize, this.uInt32_),
1133
        pack(this.ds64.riffSizeHigh, this.uInt32_),
1134
        pack(this.ds64.riffSizeLow, this.uInt32_),
1135
        pack(this.ds64.dataSizeHigh, this.uInt32_),
1136
        pack(this.ds64.dataSizeLow, this.uInt32_),
1137
        pack(this.ds64.originationTime, this.uInt32_),
1138
        pack(this.ds64.sampleCountHigh, this.uInt32_),
1139
        pack(this.ds64.sampleCountLow, this.uInt32_));
1140
    }
1141
    //if (this.ds64.tableLength) {
1142
    //  ds64Bytes = ds64Bytes.concat(
1143
    //    pack(this.ds64.tableLength, this.uInt32_),
1144
    //    this.ds64.table);
1145
    //}
1146
    return bytes;
1147
  }
1148
1149
  /**
1150
   * Return the bytes of the 'cue ' chunk.
1151
   * @return {!Array<number>} The 'cue ' chunk bytes.
1152
   * @private
1153
   */
1154
  getCueBytes_() {
1155
    /** @type {!Array<number>} */
1156
    let bytes = [];
1157
    if (this.cue.chunkId) {
1158
      /** @type {!Array<number>} */
1159
      let cuePointsBytes = this.getCuePointsBytes_();
1160
      bytes = bytes.concat(
1161
        packString(this.cue.chunkId),
1162
        pack(cuePointsBytes.length + 4, this.uInt32_),
1163
        pack(this.cue.dwCuePoints, this.uInt32_),
1164
        cuePointsBytes);
1165
    }
1166
    return bytes;
1167
  }
1168
1169
  /**
1170
   * Return the bytes of the 'cue ' points.
1171
   * @return {!Array<number>} The 'cue ' points as an array of bytes.
1172
   * @private
1173
   */
1174
  getCuePointsBytes_() {
1175
    /** @type {!Array<number>} */
1176
    let points = [];
1177
    for (let i=0; i<this.cue.dwCuePoints; i++) {
1178
      points = points.concat(
1179
        pack(this.cue.points[i].dwName, this.uInt32_),
1180
        pack(this.cue.points[i].dwPosition, this.uInt32_),
1181
        packString(this.cue.points[i].fccChunk),
1182
        pack(this.cue.points[i].dwChunkStart, this.uInt32_),
1183
        pack(this.cue.points[i].dwBlockStart, this.uInt32_),
1184
        pack(this.cue.points[i].dwSampleOffset, this.uInt32_));
1185
    }
1186
    return points;
1187
  }
1188
1189
  /**
1190
   * Return the bytes of the 'smpl' chunk.
1191
   * @return {!Array<number>} The 'smpl' chunk bytes.
1192
   * @private
1193
   */
1194
  getSmplBytes_() {
1195
    /** @type {!Array<number>} */
1196
    let bytes = [];
1197
    if (this.smpl.chunkId) {
1198
      /** @type {!Array<number>} */
1199
      let smplLoopsBytes = this.getSmplLoopsBytes_();
1200
      bytes = bytes.concat(
1201
        packString(this.smpl.chunkId),
1202
        pack(smplLoopsBytes.length + 36, this.uInt32_),
1203
        pack(this.smpl.dwManufacturer, this.uInt32_),
1204
        pack(this.smpl.dwProduct, this.uInt32_),
1205
        pack(this.smpl.dwSamplePeriod, this.uInt32_),
1206
        pack(this.smpl.dwMIDIUnityNote, this.uInt32_),
1207
        pack(this.smpl.dwMIDIPitchFraction, this.uInt32_),
1208
        pack(this.smpl.dwSMPTEFormat, this.uInt32_),
1209
        pack(this.smpl.dwSMPTEOffset, this.uInt32_),
1210
        pack(this.smpl.dwNumSampleLoops, this.uInt32_),
1211
        pack(this.smpl.dwSamplerData, this.uInt32_),
1212
        smplLoopsBytes);
1213
    }
1214
    return bytes;
1215
  }
1216
1217
  /**
1218
   * Return the bytes of the 'smpl' loops.
1219
   * @return {!Array<number>} The 'smpl' loops as an array of bytes.
1220
   * @private
1221
   */
1222
  getSmplLoopsBytes_() {
1223
    /** @type {!Array<number>} */
1224
    let loops = [];
1225
    for (let i=0; i<this.smpl.dwNumSampleLoops; i++) {
1226
      loops = loops.concat(
1227
        pack(this.smpl.loops[i].dwName, this.uInt32_),
1228
        pack(this.smpl.loops[i].dwType, this.uInt32_),
1229
        pack(this.smpl.loops[i].dwStart, this.uInt32_),
1230
        pack(this.smpl.loops[i].dwEnd, this.uInt32_),
1231
        pack(this.smpl.loops[i].dwFraction, this.uInt32_),
1232
        pack(this.smpl.loops[i].dwPlayCount, this.uInt32_));
1233
    }
1234
    return loops;
1235
  }
1236
1237
  /**
1238
   * Return the bytes of the 'fact' chunk.
1239
   * @return {!Array<number>} The 'fact' chunk bytes.
1240
   * @private
1241
   */
1242
  getFactBytes_() {
1243
    /** @type {!Array<number>} */
1244
    let bytes = [];
1245
    if (this.fact.chunkId) {
1246
      bytes = bytes.concat(
1247
        packString(this.fact.chunkId),
1248
        pack(this.fact.chunkSize, this.uInt32_),
1249
        pack(this.fact.dwSampleLength, this.uInt32_));
1250
    }
1251
    return bytes;
1252
  }
1253
1254
  /**
1255
   * Return the bytes of the 'fmt ' chunk.
1256
   * @return {!Array<number>} The 'fmt' chunk bytes.
1257
   * @throws {Error} if no 'fmt ' chunk is present.
1258
   * @private
1259
   */
1260
  getFmtBytes_() {
1261
    /** @type {!Array<number>} */
1262
    let fmtBytes = [];
1263
    if (this.fmt.chunkId) {
1264
      return fmtBytes.concat(
1265
        packString(this.fmt.chunkId),
1266
        pack(this.fmt.chunkSize, this.uInt32_),
1267
        pack(this.fmt.audioFormat, this.uInt16_),
1268
        pack(this.fmt.numChannels, this.uInt16_),
1269
        pack(this.fmt.sampleRate, this.uInt32_),
1270
        pack(this.fmt.byteRate, this.uInt32_),
1271
        pack(this.fmt.blockAlign, this.uInt16_),
1272
        pack(this.fmt.bitsPerSample, this.uInt16_),
1273
        this.getFmtExtensionBytes_());
1274
    }
1275
    throw Error('Could not find the "fmt " chunk');
1276
  }
1277
1278
  /**
1279
   * Return the bytes of the fmt extension fields.
1280
   * @return {!Array<number>} The fmt extension bytes.
1281
   * @private
1282
   */
1283
  getFmtExtensionBytes_() {
1284
    /** @type {!Array<number>} */
1285
    let extension = [];
1286
    if (this.fmt.chunkSize > 16) {
1287
      extension = extension.concat(
1288
        pack(this.fmt.cbSize, this.uInt16_));
1289
    }
1290
    if (this.fmt.chunkSize > 18) {
1291
      extension = extension.concat(
1292
        pack(this.fmt.validBitsPerSample, this.uInt16_));
1293
    }
1294
    if (this.fmt.chunkSize > 20) {
1295
      extension = extension.concat(
1296
        pack(this.fmt.dwChannelMask, this.uInt32_));
1297
    }
1298
    if (this.fmt.chunkSize > 24) {
1299
      extension = extension.concat(
1300
        pack(this.fmt.subformat[0], this.uInt32_),
1301
        pack(this.fmt.subformat[1], this.uInt32_),
1302
        pack(this.fmt.subformat[2], this.uInt32_),
1303
        pack(this.fmt.subformat[3], this.uInt32_));
1304
    }
1305
    return extension;
1306
  }
1307
1308
  /**
1309
   * Return the bytes of the 'LIST' chunk.
1310
   * @return {!Array<number>} The 'LIST' chunk bytes.
1311
   */
1312
  getLISTBytes_() {
1313
    /** @type {!Array<number>} */
1314
    let bytes = [];
1315
    for (let i=0; i<this.LIST.length; i++) {
1316
      /** @type {!Array<number>} */
1317
      let subChunksBytes = this.getLISTSubChunksBytes_(
1318
          this.LIST[i].subChunks, this.LIST[i].format);
1319
      bytes = bytes.concat(
1320
        packString(this.LIST[i].chunkId),
1321
        pack(subChunksBytes.length + 4, this.uInt32_),
1322
        packString(this.LIST[i].format),
1323
        subChunksBytes);
1324
    }
1325
    return bytes;
1326
  }
1327
1328
  /**
1329
   * Return the bytes of the sub chunks of a 'LIST' chunk.
1330
   * @param {!Array<!Object>} subChunks The 'LIST' sub chunks.
1331
   * @param {string} format The format of the 'LIST' chunk.
1332
   *    Currently supported values are 'adtl' or 'INFO'.
1333
   * @return {!Array<number>} The sub chunk bytes.
1334
   * @private
1335
   */
1336
  getLISTSubChunksBytes_(subChunks, format) {
1337
    /** @type {!Array<number>} */
1338
    let bytes = [];
1339
    for (let i=0; i<subChunks.length; i++) {
1340
      if (format == 'INFO') {
1341
        bytes = bytes.concat(
1342
          packString(subChunks[i].chunkId),
1343
          pack(subChunks[i].value.length + 1, this.uInt32_),
1344
          writeString(
1345
            subChunks[i].value, subChunks[i].value.length));
1346
        bytes.push(0);
1347
      } else if (format == 'adtl') {
1348
        if (['labl', 'note'].indexOf(subChunks[i].chunkId) > -1) {
1349
          bytes = bytes.concat(
1350
            packString(subChunks[i].chunkId),
1351
            pack(
1352
              subChunks[i].value.length + 4 + 1, this.uInt32_),
1353
            pack(subChunks[i].dwName, this.uInt32_),
1354
            writeString(
1355
              subChunks[i].value,
1356
              subChunks[i].value.length));
1357
          bytes.push(0);
1358
        } else if (subChunks[i].chunkId == 'ltxt') {
1359
          bytes = bytes.concat(
1360
            this.getLtxtChunkBytes_(subChunks[i]));
1361
        }
1362
      }
1363
      if (bytes.length % 2) {
1364
        bytes.push(0);
1365
      }
1366
    }
1367
    return bytes;
1368
  }
1369
1370
  /**
1371
   * Return the bytes of a 'ltxt' chunk.
1372
   * @param {!Object} ltxt the 'ltxt' chunk.
1373
   * @private
1374
   */
1375
  getLtxtChunkBytes_(ltxt) {
1376
    return [].concat(
1377
      packString(ltxt.chunkId),
1378
      pack(ltxt.value.length + 20, this.uInt32_),
1379
      pack(ltxt.dwName, this.uInt32_),
1380
      pack(ltxt.dwSampleLength, this.uInt32_),
1381
      pack(ltxt.dwPurposeID, this.uInt32_),
1382
      pack(ltxt.dwCountry, this.uInt16_),
1383
      pack(ltxt.dwLanguage, this.uInt16_),
1384
      pack(ltxt.dwDialect, this.uInt16_),
1385
      pack(ltxt.dwCodePage, this.uInt16_),
1386
      writeString(ltxt.value, ltxt.value.length));
1387
  }
1388
1389
  /**
1390
   * Return the bytes of the 'junk' chunk.
1391
   * @private
1392
   */
1393
  getJunkBytes_() {
1394
    /** @type {!Array<number>} */
1395
    let bytes = [];
1396
    if (this.junk.chunkId) {
1397
      return bytes.concat(
1398
        packString(this.junk.chunkId),
1399
        pack(this.junk.chunkData.length, this.uInt32_),
1400
        this.junk.chunkData);
1401
    }
1402
    return bytes;
1403
  }
1404
1405
  /**
1406
   * Set up the WaveFile object from a byte buffer.
1407
   * @param {!Uint8Array} wavBuffer The buffer.
1408
   * @param {boolean} samples True if the samples should be loaded.
1409
   * @throws {Error} If container is not RIFF, RIFX or RF64.
1410
   * @throws {Error} If format is not WAVE.
1411
   * @throws {Error} If no 'fmt ' chunk is found.
1412
   * @throws {Error} If no 'data' chunk is found.
1413
   */
1414
  readWavBuffer(wavBuffer, samples) {
1415
    this.head_ = 0;
1416
    this.readRIFFChunk(wavBuffer);
1417
    if (this.format != 'WAVE') {
1418
      throw Error('Could not find the "WAVE" format identifier');
1419
    }
1420
    this.setSignature(wavBuffer);
1421
    this.readDs64Chunk_(wavBuffer);
1422
    this.readFmtChunk_(wavBuffer);
1423
    this.readFactChunk_(wavBuffer);
1424
    this.readBextChunk_(wavBuffer);
1425
    this.readCueChunk_(wavBuffer);
1426
    this.readSmplChunk_(wavBuffer);
1427
    this.readDataChunk_(wavBuffer, samples);
1428
    this.readJunkChunk_(wavBuffer);
1429
    this.readLISTChunk_(wavBuffer);
1430
  }
1431
1432
  /**
1433
   * Read the 'fmt ' chunk of a wave file.
1434
   * @param {!Uint8Array} buffer The wav file buffer.
1435
   * @throws {Error} If no 'fmt ' chunk is found.
1436
   * @private
1437
   */
1438
  readFmtChunk_(buffer) {
1439
    /** @type {?Object} */
1440
    let chunk = this.findChunk('fmt ');
1441
    if (chunk) {
1442
      this.head_ = chunk.chunkData.start;
1443
      this.fmt.chunkId = chunk.chunkId;
1444
      this.fmt.chunkSize = chunk.chunkSize;
1445
      this.fmt.audioFormat = this.readNumber(buffer, this.uInt16_);
1446
      this.fmt.numChannels = this.readNumber(buffer, this.uInt16_);
1447
      this.fmt.sampleRate = this.readNumber(buffer, this.uInt32_);
1448
      this.fmt.byteRate = this.readNumber(buffer, this.uInt32_);
1449
      this.fmt.blockAlign = this.readNumber(buffer, this.uInt16_);
1450
      this.fmt.bitsPerSample = this.readNumber(buffer, this.uInt16_);
1451
      this.readFmtExtension_(buffer);
1452
    } else {
1453
      throw Error('Could not find the "fmt " chunk');
1454
    }
1455
  }
1456
1457
  /**
1458
   * Read the 'fmt ' chunk extension.
1459
   * @param {!Uint8Array} buffer The wav file buffer.
1460
   * @private
1461
   */
1462
  readFmtExtension_(buffer) {
1463
    if (this.fmt.chunkSize > 16) {
1464
      this.fmt.cbSize = this.readNumber(buffer, this.uInt16_);
1465
      if (this.fmt.chunkSize > 18) {
1466
        this.fmt.validBitsPerSample = this.readNumber(buffer, this.uInt16_);
1467
        if (this.fmt.chunkSize > 20) {
1468
          this.fmt.dwChannelMask = this.readNumber(buffer, this.uInt32_);
1469
          this.fmt.subformat = [
1470
            this.readNumber(buffer, this.uInt32_),
1471
            this.readNumber(buffer, this.uInt32_),
1472
            this.readNumber(buffer, this.uInt32_),
1473
            this.readNumber(buffer, this.uInt32_)];
1474
        }
1475
      }
1476
    }
1477
  }
1478
1479
  /**
1480
   * Read the 'fact' chunk of a wav file.
1481
   * @param {!Uint8Array} buffer The wav file buffer.
1482
   * @private
1483
   */
1484
  readFactChunk_(buffer) {
1485
    /** @type {?Object} */
1486
    let chunk = this.findChunk('fact');
1487
    if (chunk) {
1488
      this.head_ = chunk.chunkData.start;
1489
      this.fact.chunkId = chunk.chunkId;
1490
      this.fact.chunkSize = chunk.chunkSize;
1491
      this.fact.dwSampleLength = this.readNumber(buffer, this.uInt32_);
1492
    }
1493
  }
1494
1495
  /**
1496
   * Read the 'cue ' chunk of a wave file.
1497
   * @param {!Uint8Array} buffer The wav file buffer.
1498
   * @private
1499
   */
1500
  readCueChunk_(buffer) {
1501
    /** @type {?Object} */
1502
    let chunk = this.findChunk('cue ');
1503
    if (chunk) {
1504
      this.head_ = chunk.chunkData.start;
1505
      this.cue.chunkId = chunk.chunkId;
1506
      this.cue.chunkSize = chunk.chunkSize;
1507
      this.cue.dwCuePoints = this.readNumber(buffer, this.uInt32_);
1508
      for (let i = 0; i < this.cue.dwCuePoints; i++) {
1509
        this.cue.points.push({
1510
          dwName: this.readNumber(buffer, this.uInt32_),
1511
          dwPosition: this.readNumber(buffer, this.uInt32_),
1512
          fccChunk: this.readString(buffer, 4),
1513
          dwChunkStart: this.readNumber(buffer, this.uInt32_),
1514
          dwBlockStart: this.readNumber(buffer, this.uInt32_),
1515
          dwSampleOffset: this.readNumber(buffer, this.uInt32_),
1516
        });
1517
      }
1518
    }
1519
  }
1520
1521
  /**
1522
   * Read the 'smpl' chunk of a wave file.
1523
   * @param {!Uint8Array} buffer The wav file buffer.
1524
   * @private
1525
   */
1526
  readSmplChunk_(buffer) {
1527
    /** @type {?Object} */
1528
    let chunk = this.findChunk('smpl');
1529
    if (chunk) {
1530
      this.head_ = chunk.chunkData.start;
1531
      this.smpl.chunkId = chunk.chunkId;
1532
      this.smpl.chunkSize = chunk.chunkSize;
1533
      this.smpl.dwManufacturer = this.readNumber(buffer, this.uInt32_);
1534
      this.smpl.dwProduct = this.readNumber(buffer, this.uInt32_);
1535
      this.smpl.dwSamplePeriod = this.readNumber(buffer, this.uInt32_);
1536
      this.smpl.dwMIDIUnityNote = this.readNumber(buffer, this.uInt32_);
1537
      this.smpl.dwMIDIPitchFraction = this.readNumber(buffer, this.uInt32_);
1538
      this.smpl.dwSMPTEFormat = this.readNumber(buffer, this.uInt32_);
1539
      this.smpl.dwSMPTEOffset = this.readNumber(buffer, this.uInt32_);
1540
      this.smpl.dwNumSampleLoops = this.readNumber(buffer, this.uInt32_);
1541
      this.smpl.dwSamplerData = this.readNumber(buffer, this.uInt32_);
1542
      for (let i = 0; i < this.smpl.dwNumSampleLoops; i++) {
1543
        this.smpl.loops.push({
1544
          dwName: this.readNumber(buffer, this.uInt32_),
1545
          dwType: this.readNumber(buffer, this.uInt32_),
1546
          dwStart: this.readNumber(buffer, this.uInt32_),
1547
          dwEnd: this.readNumber(buffer, this.uInt32_),
1548
          dwFraction: this.readNumber(buffer, this.uInt32_),
1549
          dwPlayCount: this.readNumber(buffer, this.uInt32_),
1550
        });
1551
      }
1552
    }
1553
  }
1554
1555
  /**
1556
   * Read the 'data' chunk of a wave file.
1557
   * @param {!Uint8Array} buffer The wav file buffer.
1558
   * @param {boolean} samples True if the samples should be loaded.
1559
   * @throws {Error} If no 'data' chunk is found.
1560
   * @private
1561
   */
1562
  readDataChunk_(buffer, samples) {
1563
    /** @type {?Object} */
1564
    let chunk = this.findChunk('data');
1565
    if (chunk) {
1566
      this.data.chunkId = 'data';
1567
      this.data.chunkSize = chunk.chunkSize;
1568
      if (samples) {
1569
        this.data.samples = buffer.slice(
1570
          chunk.chunkData.start,
1571
          chunk.chunkData.end);
1572
      }
1573
    } else {
1574
      throw Error('Could not find the "data" chunk');
1575
    }
1576
  }
1577
1578
  /**
1579
   * Read the 'bext' chunk of a wav file.
1580
   * @param {!Uint8Array} buffer The wav file buffer.
1581
   * @private
1582
   */
1583
  readBextChunk_(buffer) {
1584
    /** @type {?Object} */
1585
    let chunk = this.findChunk('bext');
1586
    if (chunk) {
1587
      this.head_ = chunk.chunkData.start;
1588
      this.bext.chunkId = chunk.chunkId;
1589
      this.bext.chunkSize = chunk.chunkSize;
1590
      this.bext.description = this.readString(buffer, 256);
1591
      this.bext.originator = this.readString(buffer, 32);
1592
      this.bext.originatorReference = this.readString(buffer, 32);
1593
      this.bext.originationDate = this.readString(buffer, 10);
1594
      this.bext.originationTime = this.readString(buffer, 8);
1595
      this.bext.timeReference = [
1596
        this.readNumber(buffer, this.uInt32_),
1597
        this.readNumber(buffer, this.uInt32_)];
1598
      this.bext.version = this.readNumber(buffer, this.uInt16_);
1599
      this.bext.UMID = this.readString(buffer, 64);
1600
      this.bext.loudnessValue = this.readNumber(buffer, this.uInt16_);
1601
      this.bext.loudnessRange = this.readNumber(buffer, this.uInt16_);
1602
      this.bext.maxTruePeakLevel = this.readNumber(buffer, this.uInt16_);
1603
      this.bext.maxMomentaryLoudness = this.readNumber(buffer, this.uInt16_);
1604
      this.bext.maxShortTermLoudness = this.readNumber(buffer, this.uInt16_);
1605
      this.bext.reserved = this.readString(buffer, 180);
1606
      this.bext.codingHistory = this.readString(
1607
        buffer, this.bext.chunkSize - 602);
1608
    }
1609
  }
1610
1611
  /**
1612
   * Read the 'ds64' chunk of a wave file.
1613
   * @param {!Uint8Array} buffer The wav file buffer.
1614
   * @throws {Error} If no 'ds64' chunk is found and the file is RF64.
1615
   * @private
1616
   */
1617
  readDs64Chunk_(buffer) {
1618
    /** @type {?Object} */
1619
    let chunk = this.findChunk('ds64');
1620
    if (chunk) {
1621
      this.head_ = chunk.chunkData.start;
1622
      this.ds64.chunkId = chunk.chunkId;
1623
      this.ds64.chunkSize = chunk.chunkSize;
1624
      this.ds64.riffSizeHigh = this.readNumber(buffer, this.uInt32_);
1625
      this.ds64.riffSizeLow = this.readNumber(buffer, this.uInt32_);
1626
      this.ds64.dataSizeHigh = this.readNumber(buffer, this.uInt32_);
1627
      this.ds64.dataSizeLow = this.readNumber(buffer, this.uInt32_);
1628
      this.ds64.originationTime = this.readNumber(buffer, this.uInt32_);
1629
      this.ds64.sampleCountHigh = this.readNumber(buffer, this.uInt32_);
1630
      this.ds64.sampleCountLow = this.readNumber(buffer, this.uInt32_);
1631
      //if (wav.ds64.chunkSize > 28) {
1632
      //  wav.ds64.tableLength = unpack(
1633
      //    chunkData.slice(28, 32), uInt32_);
1634
      //  wav.ds64.table = chunkData.slice(
1635
      //     32, 32 + wav.ds64.tableLength);
1636
      //}
1637
    } else {
1638
      if (this.container == 'RF64') {
1639
        throw Error('Could not find the "ds64" chunk');
1640
      }
1641
    }
1642
  }
1643
1644
  /**
1645
   * Read the 'LIST' chunks of a wave file.
1646
   * @param {!Uint8Array} buffer The wav file buffer.
1647
   * @private
1648
   */
1649
  readLISTChunk_(buffer) {
1650
    /** @type {?Object} */
1651
    let listChunks = this.findChunk('LIST', true);
1652
    if (listChunks !== null) {
1653
      for (let j=0; j < listChunks.length; j++) {
1654
        /** @type {!Object} */
1655
        let subChunk = listChunks[j];
1656
        this.LIST.push({
1657
          chunkId: subChunk.chunkId,
1658
          chunkSize: subChunk.chunkSize,
1659
          format: subChunk.format,
1660
          subChunks: []});
1661
        for (let x=0; x<subChunk.subChunks.length; x++) {
1662
          this.readLISTSubChunks_(subChunk.subChunks[x],
1663
            subChunk.format, buffer);
1664
        }
1665
      }
1666
    }
1667
  }
1668
1669
  /**
1670
   * Read the sub chunks of a 'LIST' chunk.
1671
   * @param {!Object} subChunk The 'LIST' subchunks.
1672
   * @param {string} format The 'LIST' format, 'adtl' or 'INFO'.
1673
   * @param {!Uint8Array} buffer The wav file buffer.
1674
   * @private
1675
   */
1676
  readLISTSubChunks_(subChunk, format, buffer) {
1677
    if (format == 'adtl') {
1678
      if (['labl', 'note','ltxt'].indexOf(subChunk.chunkId) > -1) {
1679
        this.head_ = subChunk.chunkData.start;
1680
        /** @type {!Object<string, string|number>} */
1681
        let item = {
1682
          chunkId: subChunk.chunkId,
1683
          chunkSize: subChunk.chunkSize,
1684
          dwName: this.readNumber(buffer, this.uInt32_)
1685
        };
1686
        if (subChunk.chunkId == 'ltxt') {
1687
          item.dwSampleLength = this.readNumber(buffer, this.uInt32_);
1688
          item.dwPurposeID = this.readNumber(buffer, this.uInt32_);
1689
          item.dwCountry = this.readNumber(buffer, this.uInt16_);
1690
          item.dwLanguage = this.readNumber(buffer, this.uInt16_);
1691
          item.dwDialect = this.readNumber(buffer, this.uInt16_);
1692
          item.dwCodePage = this.readNumber(buffer, this.uInt16_);
1693
        }
1694
        item.value = this.readZSTR(buffer, this.head_);
1695
        this.LIST[this.LIST.length - 1].subChunks.push(item);
1696
      }
1697
    // RIFF INFO tags like ICRD, ISFT, ICMT
1698
    } else if(format == 'INFO') {
1699
      this.head_ = subChunk.chunkData.start;
1700
      this.LIST[this.LIST.length - 1].subChunks.push({
1701
        chunkId: subChunk.chunkId,
1702
        chunkSize: subChunk.chunkSize,
1703
        value: this.readZSTR(buffer, this.head_)
1704
      });
1705
    }
1706
  }
1707
1708
  /**
1709
   * Read the 'junk' chunk of a wave file.
1710
   * @param {!Uint8Array} buffer The wav file buffer.
1711
   * @private
1712
   */
1713
  readJunkChunk_(buffer) {
1714
    /** @type {?Object} */
1715
    let chunk = this.findChunk('junk');
1716
    if (chunk) {
1717
      this.junk = {
1718
        chunkId: chunk.chunkId,
1719
        chunkSize: chunk.chunkSize,
1720
        chunkData: [].slice.call(buffer.slice(
1721
          chunk.chunkData.start,
1722
          chunk.chunkData.end))
1723
      };
1724
    }
1725
  }
1726
1727
  /**
1728
   * Define the header of a wav file.
1729
   * @param {string} bitDepthCode The audio bit depth
1730
   * @param {number} numChannels The number of channels
1731
   * @param {number} sampleRate The sample rate.
1732
   * @param {number} numBytes The number of bytes each sample use.
1733
   * @param {number} samplesLength The length of the samples in bytes.
1734
   * @param {!Object} options The extra options, like container defintion.
1735
   * @private
1736
   */
1737
  makeWavHeader(
1738
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
1739
    if (bitDepthCode == '4') {
1740
      this.createADPCMHeader_(
1741
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
1742
1743
    } else if (bitDepthCode == '8a' || bitDepthCode == '8m') {
1744
      this.createALawMulawHeader_(
1745
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
1746
1747
    } else if(Object.keys(this.WAV_AUDIO_FORMATS).indexOf(bitDepthCode) == -1 ||
1748
        numChannels > 2) {
1749
      this.createExtensibleHeader_(
1750
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
1751
1752
    } else {
1753
      this.createPCMHeader_(
1754
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);      
1755
    }
1756
  }
1757
1758
  /**
1759
   * Create the header of a linear PCM wave file.
1760
   * @param {string} bitDepthCode The audio bit depth
1761
   * @param {number} numChannels The number of channels
1762
   * @param {number} sampleRate The sample rate.
1763
   * @param {number} numBytes The number of bytes each sample use.
1764
   * @param {number} samplesLength The length of the samples in bytes.
1765
   * @param {!Object} options The extra options, like container defintion.
1766
   * @private
1767
   */
1768
  createPCMHeader_(
1769
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
1770
    this.container = options.container;
1771
    this.chunkSize = 36 + samplesLength;
1772
    this.format = 'WAVE';
1773
    this.bitDepth = bitDepthCode;
1774
    this.fmt = {
1775
      chunkId: 'fmt ',
1776
      chunkSize: 16,
1777
      audioFormat: this.WAV_AUDIO_FORMATS[bitDepthCode] || 65534,
1778
      numChannels: numChannels,
1779
      sampleRate: sampleRate,
1780
      byteRate: (numChannels * numBytes) * sampleRate,
1781
      blockAlign: numChannels * numBytes,
1782
      bitsPerSample: parseInt(bitDepthCode, 10),
1783
      cbSize: 0,
1784
      validBitsPerSample: 0,
1785
      dwChannelMask: 0,
1786
      subformat: []
1787
    };
1788
  }
1789
1790
  /**
1791
   * Create the header of a ADPCM wave file.
1792
   * @param {string} bitDepthCode The audio bit depth
1793
   * @param {number} numChannels The number of channels
1794
   * @param {number} sampleRate The sample rate.
1795
   * @param {number} numBytes The number of bytes each sample use.
1796
   * @param {number} samplesLength The length of the samples in bytes.
1797
   * @param {!Object} options The extra options, like container defintion.
1798
   * @private
1799
   */
1800
  createADPCMHeader_(
1801
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
1802
    this.createPCMHeader_(
1803
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
1804
    this.chunkSize = 40 + samplesLength;
1805
    this.fmt.chunkSize = 20;
1806
    this.fmt.byteRate = 4055;
1807
    this.fmt.blockAlign = 256;
1808
    this.fmt.bitsPerSample = 4;
1809
    this.fmt.cbSize = 2;
1810
    this.fmt.validBitsPerSample = 505;
1811
    this.fact = {
1812
      chunkId: 'fact',
1813
      chunkSize: 4,
1814
      dwSampleLength: samplesLength * 2
1815
    };
1816
  }
1817
1818
  /**
1819
   * Create the header of WAVE_FORMAT_EXTENSIBLE file.
1820
   * @param {string} bitDepthCode The audio bit depth
1821
   * @param {number} numChannels The number of channels
1822
   * @param {number} sampleRate The sample rate.
1823
   * @param {number} numBytes The number of bytes each sample use.
1824
   * @param {number} samplesLength The length of the samples in bytes.
1825
   * @param {!Object} options The extra options, like container defintion.
1826
   * @private
1827
   */
1828
  createExtensibleHeader_(
1829
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
1830
    this.createPCMHeader_(
1831
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
1832
    this.chunkSize = 36 + 24 + samplesLength;
1833
    this.fmt.chunkSize = 40;
1834
    this.fmt.bitsPerSample = ((parseInt(bitDepthCode, 10) - 1) | 7) + 1;
1835
    this.fmt.cbSize = 22;
1836
    this.fmt.validBitsPerSample = parseInt(bitDepthCode, 10);
1837
    this.fmt.dwChannelMask = dwChannelMask(numChannels);
1838
    // subformat 128-bit GUID as 4 32-bit values
1839
    // only supports uncompressed integer PCM samples
1840
    this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
1841
  }
1842
1843
  /**
1844
   * Create the header of mu-Law and A-Law wave files.
1845
   * @param {string} bitDepthCode The audio bit depth
1846
   * @param {number} numChannels The number of channels
1847
   * @param {number} sampleRate The sample rate.
1848
   * @param {number} numBytes The number of bytes each sample use.
1849
   * @param {number} samplesLength The length of the samples in bytes.
1850
   * @param {!Object} options The extra options, like container defintion.
1851
   * @private
1852
   */
1853
  createALawMulawHeader_(
1854
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
1855
    this.createPCMHeader_(
1856
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
1857
    this.chunkSize = 40 + samplesLength;
1858
    this.fmt.chunkSize = 20;
1859
    this.fmt.cbSize = 2;
1860
    this.fmt.validBitsPerSample = 8;
1861
    this.fact = {
1862
      chunkId: 'fact',
1863
      chunkSize: 4,
1864
      dwSampleLength: samplesLength
1865
    };
1866
  }
1867
1868
  /**
1869
   * Validate the header of the file.
1870
   * @throws {Error} If any property of the object appears invalid.
1871
   * @private
1872
   */
1873
  validateWavHeader_() {
1874
    this.validateBitDepth_();
1875
    this.validateNumChannels_();
1876
    this.validateSampleRate_();
1877
  }
1878
1879
  /**
1880
   * Validate the bit depth.
1881
   * @return {boolean} True is the bit depth is valid.
1882
   * @throws {Error} If bit depth is invalid.
1883
   * @private
1884
   */
1885
  validateBitDepth_() {
1886
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
1887
      if (parseInt(this.bitDepth, 10) > 8 &&
1888
          parseInt(this.bitDepth, 10) < 54) {
1889
        return true;
1890
      }
1891
      throw new Error('Invalid bit depth.');
1892
    }
1893
    return true;
1894
  }
1895
1896
  /**
1897
   * Validate the number of channels.
1898
   * @return {boolean} True is the number of channels is valid.
1899
   * @throws {Error} If the number of channels is invalid.
1900
   * @private
1901
   */
1902
  validateNumChannels_() {
1903
    /** @type {number} */
1904
    let blockAlign = this.fmt.numChannels * this.fmt.bitsPerSample / 8;
1905
    if (this.fmt.numChannels < 1 || blockAlign > 65535) {
1906
      throw new Error('Invalid number of channels.');
1907
    }
1908
    return true;
1909
  }
1910
1911
  /**
1912
   * Validate the sample rate value.
1913
   * @return {boolean} True is the sample rate is valid.
1914
   * @throws {Error} If the sample rate is invalid.
1915
   * @private
1916
   */
1917
  validateSampleRate_() {
1918
    /** @type {number} */
1919
    let byteRate = this.fmt.numChannels *
1920
      (this.fmt.bitsPerSample / 8) * this.fmt.sampleRate;
1921
    if (this.fmt.sampleRate < 1 || byteRate > 4294967295) {
1922
      throw new Error('Invalid sample rate.');
1923
    }
1924
    return true;
1925
  }
1926
}
1927